<?php
declare(strict_types=1);

namespace SixShop\WechatPay;

use GuzzleHttp\Exception\ClientException;
use SixShop\Core\Exception\NotFoundException;
use SixShop\Payment\Contracts\PaymentNotifyResult;
use SixShop\Payment\Contracts\PaymentProviderInterface;
use SixShop\Payment\Contracts\PaymentQueryResult;
use SixShop\Payment\Contracts\PaymentRefundQueryResult;
use SixShop\Payment\Contracts\PaymentRefundRequest;
use SixShop\Payment\Contracts\PaymentRefundResult;
use SixShop\Payment\Contracts\PaymentResponse;
use SixShop\Payment\Entity\ExtensionPaymentEntity;
use SixShop\Payment\Entity\ExtensionRefundEntity;
use SixShop\Payment\Enum\PaymentBizEnum;
use SixShop\Payment\Enum\PaymentStatusEnum;
use SixShop\Payment\Enum\RefundStatusEnum;
use SixShop\Payment\Event\PaymentSuccessEvent;
use SixShop\Payment\Event\RefundSuccessEvent;
use SixShop\Wechat\Facade\WechatUser;
use SixShop\WechatPay\Entity\WechatpayTransferBillEntity;
use SixShop\WechatPay\Job\QueryRefundJob;
use SixShop\WechatPay\Service\NotifyService;
use SixShop\WechatPay\Trait\ApiTrait;
use SixShop\WechatPay\Trait\MiniAppTrait;
use SixShop\WechatPay\Trait\PaymentParamsTrait;
use think\facade\Db;
use think\facade\Event;
use think\facade\Log;
use function SixShop\Core\throw_logic_exception;

class PaymentProvider implements PaymentProviderInterface
{
    private const string PAYMENT_TYPE = 'wechatpay';
    use ApiTrait;
    use MiniAppTrait {
        uploadShippingInfo as private uploadShippingInfoAPI;
        waybillToken as private waybillTokenAPI;
    }
    use PaymentParamsTrait;

    public function __construct(
        private readonly ExtensionPaymentEntity $extensionPaymentEntity,
        private readonly ExtensionRefundEntity  $extensionRefundEntity,
        private readonly WechatpayTransferBillEntity $wechatpayTransferBillEntity,
        private readonly NotifyService $notifyService,
    )
    {
    }

    public function create(array $order, PaymentBizEnum $bizType): PaymentResponse
    {
        $payment = $this->extensionPaymentEntity->where([
            'order_id' => $order['id'],
            'pay_type' => self::PAYMENT_TYPE,
            'biz_type' => $bizType,
        ])->findOrEmpty();
        if (!$payment->isEmpty()) {
            // todo 判断订单是否支付成功
            // 支付时间结束关闭订单
            // 订单未支付可重新支付
            // 交易已关闭请重新下单
            //订单已付款请勿重复操作
            // 订单已退款请重新下单
            //订单已撤销请重新下单
            // 订单支付中请稍后再试
            // 订单支付失败请重新下单
            throw new  \RuntimeException('开发测试中，请稍后再试');
        }
        $payment->transaction(function () use ($bizType, $order, $payment) {
            $payment->save([
                'user_id' => $order['user_id'],
                'order_id' => $order['id'],
                'order_sn' => $order['order_sn'],
                'biz_type' => $bizType,
                'pay_type' => self::PAYMENT_TYPE,
                'amount' => $order['pay_amount'],
                'status' => PaymentStatusEnum::PENDING
            ]);
            $expireTime = time() + 3600;
            $payment->expire_time = $expireTime;
            $openid = WechatUser::openid($order['user_id']);
            if ($openid === null) {
                throw_logic_exception('用户需要先使用微信登录绑定微信身份');
            }
            $payment->payment_param = $this->wechatPay(
                openid: $openid,
                outTradeNo: $payment['out_trade_no'],
                total: (int)($payment['amount'] * 100),
                description: ($order['description'] ?? '') ?: '订单：' . $order['order_sn'],
                expireTime: $expireTime
            );
            $payment->payment_param = $this->paymentParams($payment->payment_param->prepay_id);
            $payment->save();
        });
        return new PaymentResponse(orderNo: $payment->out_trade_no, type: self::PAYMENT_TYPE, raw: $payment->toArray());
    }

    /**
     * 支付成功通知
     *
     * @param array{headers: array<string, string>, inBody: string} $request
     * @return PaymentNotifyResult
     * @throws \Exception]
     */
    public function notify(array $request): PaymentNotifyResult
    {
        $inBody = $request['inBody'];
        $data = $this->notifyService->transactionSuccess($request['headers'], json_encode($request['inBody']));
        if ($inBody['event_type'] == 'TRANSACTION.SUCCESS') {
            // 交易成功
            Log::debug(__METHOD__ . json_encode($data));
            $payment = $this->extensionPaymentEntity->where([
                'out_trade_no' => $data['out_trade_no'],
            ])->findOrEmpty();
            if ($payment->isEmpty()) {
                throw new \RuntimeException('订单不存在或已结束');
            }
            // 验签有问题，暂时叫个支付查询处理 todo 待完善
            $queryResult = $this->query($payment['id']);
            return new PaymentNotifyResult(
                orderNo: $queryResult->orderNo,
                transactionId: $data['transaction_id'],
                amount: $queryResult->amount,
                status: $queryResult->status,
                raw: $data
            );
        } else if ($inBody['event_type'] == 'MCHTRANSFER.BILL.FINISHED') {
            // 转账完成
            $transferBill = $this->wechatpayTransferBillEntity->where('out_bill_no', $data['out_bill_no'])->findOrEmpty();
            if ($transferBill->isEmpty()) {
                throw new \RuntimeException('转账单不存在');
            }
            $this->wechatpayTransferBillEntity->refreshTransferBill($transferBill->id);
            return new PaymentNotifyResult(
                orderNo: $transferBill['out_bill_no'],
                transactionId: $transferBill['transfer_bill_no'],
                amount: round($transferBill['transfer_amount'] / 100, 2),
                status: PaymentStatusEnum::SUCCESS,
                raw: $data
            );
        }
        Log::warning('Not implemented: ' . $inBody['event_type']. ' ' . json_encode($data));
        throw_logic_exception('Not implemented: ' . $inBody['event_type']);
    }

    public function query(int $recordID): PaymentQueryResult
    {
        $payment = $this->extensionPaymentEntity->findOrEmpty($recordID);
        if ($payment->status === PaymentStatusEnum::PENDING) {
            try {
                $paymentResult = $this->queryByOutTradeNo($payment['out_trade_no']);
            } catch (ClientException $e) {
                if ($e->getCode() === 404) {
                    throw new NotFoundException(sprintf('订单%s不存在', $payment['out_trade_no']));
                }
                throw $e;
            }
            $payment->payment_result = $paymentResult;
            if ($paymentResult->trade_state === 'SUCCESS') {
                $payment->transaction_id = $paymentResult->transaction_id;
                $payment->payment_time = strtotime($paymentResult->success_time);
                $payment->status = PaymentStatusEnum::SUCCESS;
                $payment->save();
                Event::trigger(new PaymentSuccessEvent($payment['order_sn'], self::PAYMENT_TYPE, $payment->toArray(), $payment->biz_type));
            } else {
                throw_logic_exception($paymentResult->trade_state_desc);
            }
        }

        return new PaymentQueryResult(
            orderNo: $payment['out_trade_no'],
            status: $payment['status'],
            amount: (float)$payment['amount'],
            raw: $payment->toArray()
        );
    }

    public function refund(int $recordID, PaymentRefundRequest $param): PaymentRefundResult
    {
        $payment = $this->extensionPaymentEntity->find($recordID);
        $refund = Db::transaction(function () use ($param, $payment) {
            $refund = $this->extensionRefundEntity->create([
                'payment_id' => $payment->id,
                'order_sn' => $payment->out_trade_no,
                'reason' => $param->getReason(),
                'amount' => $param->getAmount(),
                'status' => RefundStatusEnum::REFUNDING,
                'refund_param' => $param->getRaw(),
                'status_desc' => '正在申请微信接口退款',
            ]);
            $result = $this->domesticRefunds(
                $refund->out_refund_no,
                $payment->out_trade_no,
                $param->getAmount(),
                $payment->amount,
                $param->getReason()
            );
            $refund->refund_id = $result->refund_id;
            $refund->refund_result = $result;
            $refund->save();
            if ($result->status === 'SUCCESS' || $result->status === 'PROCESSING') {
                QueryRefundJob::dispatch($refund->id)->delay(20);
            } else {
                throw new \RuntimeException(match ($result->status) {
                    'CLOSED' => '退款关闭',
                    'ABNORMAL' => '退款异常',
                    default => '未知错误',
                });
            }
            return $refund;
        });
        return new PaymentRefundResult($refund);
    }

    public function refundQuery(int $refundID): PaymentRefundResult
    {
        $refund = $this->extensionRefundEntity->with('payment')->find($refundID);
        if ($refund->status === RefundStatusEnum::REFUNDING) {
            $result = $this->queryRefund($refund->out_refund_no);
            $refund->refund_result = $result;
            if ($result->status === 'SUCCESS') {
                $refund->status = RefundStatusEnum::SUCCESS;
                $refund->status_desc = '成功退款到' . $result->user_received_account;
                $refund->success_time = strtotime($result->success_time);
                Event::trigger(new RefundSuccessEvent(
                        $refund->model(),
                        $refund->payment,
                        new PaymentRefundRequest($refund->amount, $refund->reason, $refund->refund_param))
                );
            } else if ($result->status === 'PROCESSING') {
                QueryRefundJob::dispatch($refund->id)->delay(10);
            }
            $refund->save();
        }

        return new PaymentRefundResult($refund->model());
    }

    /**
     * 发货信息录入
     * @param int $orderID 订单ID
     * @param int $bizType 业务类型 
     * @param string $itemDesc 商品描述
     * @param string $trackingNo 运单号
     * @param string $expressCompany 快递公司ID
     * @param string $receiverContact 收件人手机号码
     * @param int $logisticsType 配送方式
     * @param bool $failException 是否抛出异常
     */
    public function uploadShippingInfo(
        int    $orderID,
        int  $bizType = 1,
        string $itemDesc = '',
        string $trackingNo = '',
        string $expressCompany = '',
        string $receiverContact = '',
        int $logisticsType = 1,
        bool   $failException = true,
    ): array
    {
        $order = $this->extensionPaymentEntity->where([
            'order_id' => $orderID,
            'biz_type' => $bizType,
            'status' => PaymentStatusEnum::SUCCESS,
            'pay_type' => self::PAYMENT_TYPE,
        ])->findOrEmpty();
        if ($order->isEmpty()) {
            throw new \RuntimeException('支付订单不存在或未支付');
        }
        return $this->uploadShippingInfoAPI($order->out_trade_no, $order->user_id, $itemDesc, $trackingNo, $expressCompany, $receiverContact, $logisticsType, $failException);
    }


    /**
     * 传运单接口 trace_waybill
     */
    public function waybillToken(
        int $orderID,
        string $receiverPhone,
        string $waybillID,
        string $deliveryID,
        array $detailList,
        bool   $failException = true
    ): array
    {
        $order = $this->extensionPaymentEntity->where([
            'order_id' => $orderID,
            'biz_type' => 1,
            'status' => PaymentStatusEnum::SUCCESS,
            'pay_type' => self::PAYMENT_TYPE
        ])->findOrEmpty();
        if ($order->isEmpty()) {
            throw new \RuntimeException('支付订单不存在或未支付');
        }
        return $this->waybillTokenAPI($order->user_id, $receiverPhone, $waybillID, $deliveryID, $detailList, $failException);
    }
}